/** * This file is part of Waarp Project. * * Copyright 2009, Frederic Bregier, and individual contributors by the @author tags. See the * COPYRIGHT.txt in the distribution for a full listing of individual contributors. * * All Waarp Project is free software: you can redistribute it and/or modify it under the terms of * the GNU General Public License as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * * You should have received a copy of the GNU General Public License along with Waarp . If not, see * <http://www.gnu.org/licenses/>. */ package org.waarp.ftp.core.data.handler; import java.io.IOException; import java.net.BindException; import java.net.ConnectException; import java.nio.channels.CancelledKeyException; import java.nio.channels.ClosedChannelException; import java.nio.channels.NotYetConnectedException; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelException; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.channel.SimpleChannelInboundHandler; import org.waarp.common.crypto.ssl.WaarpSslUtility; import org.waarp.common.exception.FileTransferException; import org.waarp.common.exception.InvalidArgumentException; import org.waarp.common.file.DataBlock; import org.waarp.common.logging.WaarpLogger; import org.waarp.common.logging.WaarpLoggerFactory; import org.waarp.common.utility.WaarpStringUtils; import org.waarp.ftp.core.config.FtpConfiguration; import org.waarp.ftp.core.config.FtpInternalConfiguration; import org.waarp.ftp.core.control.NetworkHandler; import org.waarp.ftp.core.data.FtpTransfer; import org.waarp.ftp.core.data.FtpTransferControl; import org.waarp.ftp.core.exception.FtpNoConnectionException; import org.waarp.ftp.core.exception.FtpNoFileException; import org.waarp.ftp.core.exception.FtpNoTransferException; import org.waarp.ftp.core.session.FtpSession; import org.waarp.ftp.core.utils.FtpChannelUtils; /** * Network handler for Data connections * * @author Frederic Bregier * */ public class DataNetworkHandler extends SimpleChannelInboundHandler<DataBlock> { /** * Internal Logger */ private static final WaarpLogger logger = WaarpLoggerFactory .getLogger(DataNetworkHandler.class); /** * Business Data Handler */ private DataBusinessHandler dataBusinessHandler = null; /** * Configuration */ protected final FtpConfiguration configuration; /** * Is this Data Connection an Active or Passive one */ private final boolean isActive; /** * Internal store for the SessionInterface */ protected FtpSession session = null; /** * The associated Channel */ private Channel dataChannel = null; /** * Pipeline */ private ChannelPipeline channelPipeline = null; /** * The associated FtpTransfer */ private volatile FtpTransfer ftpTransfer = null; /** * Constructor from DataBusinessHandler * * @param configuration * @param handler * @param active */ public DataNetworkHandler(FtpConfiguration configuration, DataBusinessHandler handler, boolean active) { super(); this.configuration = configuration; dataBusinessHandler = handler; dataBusinessHandler.setDataNetworkHandler(this); isActive = active; } /** * @return the dataBusinessHandler * @throws FtpNoConnectionException */ public DataBusinessHandler getDataBusinessHandler() throws FtpNoConnectionException { if (dataBusinessHandler == null) { throw new FtpNoConnectionException("No Data Connection active"); } return dataBusinessHandler; } /** * @return the session */ public FtpSession getFtpSession() { return session; } /** * * @return the NetworkHandler associated with the control connection */ public NetworkHandler getNetworkHandler() { return session.getBusinessHandler().getNetworkHandler(); } /** * Run firstly executeChannelClosed. * * @throws Exception */ @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { logger.debug("Data Channel closed with a session ? "+(session !=null)); if (session != null) { if (session.getDataConn().checkCorrectChannel(ctx.channel())) { session.getDataConn().getFtpTransferControl().setPreEndOfTransfer(); } else { session.getDataConn().getFtpTransferControl().setTransferAbortedFromInternal(true); } session.getDataConn().unbindPassive(); try { getDataBusinessHandler().executeChannelClosed(); // release file and other permanent objects getDataBusinessHandler().clear(); } catch (FtpNoConnectionException e1) { } dataBusinessHandler = null; channelPipeline = null; dataChannel = null; } super.channelInactive(ctx); } protected void setSession(Channel channel) { // First get the ftpSession from inetaddresses for (int i = 0; i < FtpInternalConfiguration.RETRYNB; i++) { session = configuration.getFtpSession(channel, isActive); if (session == null) { logger.warn("Session not found at try " + i); try { Thread.sleep(FtpInternalConfiguration.RETRYINMS); } catch (InterruptedException e1) { break; } } else { break; } } if (session == null) { // Not found !!! logger.error("Session not found!"); WaarpSslUtility.closingSslChannel(channel); // Problem: control connection could not be directly informed!!! // Only timeout will occur return; } } /** * Initialize the Handler. * */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { Channel channel = ctx.channel(); channel.config().setAutoRead(false); if (session == null) { setSession(channel); } logger.debug("Data Channel opened as "+channel); if (session == null) { logger.debug("DataChannel immediately closed since no session is assigned"); WaarpSslUtility.closingSslChannel(ctx.channel()); return; } channelPipeline = ctx.pipeline(); dataChannel = channel; dataBusinessHandler.setFtpSession(getFtpSession()); FtpChannelUtils.addDataChannel(channel, session.getConfiguration()); logger.debug("DataChannel connected: " + session.getReplyCode()); if (session.getReplyCode().getCode() >= 400) { // shall not be except if an error early occurs switch (session.getCurrentCommand().getCode()) { case RETR: case APPE: case STOR: case STOU: // close the data channel immediately logger.debug("DataChannel immediately closed since " + session.getCurrentCommand().getCode() + " is not ok at startup"); WaarpSslUtility.closingSslChannel(ctx.channel()); return; default: break; } } if (isStillAlive()) { setCorrectCodec(); unlockModeCodec(); session.getDataConn().getFtpTransferControl().setOpenedDataChannel(channel, this); logger.debug("DataChannel fully configured"); } else { // Cannot continue logger.debug("Connected but no more alive so will disconnect"); session.getDataConn().getFtpTransferControl().setOpenedDataChannel(null, this); return; } } /** * Set the CODEC according to the mode. Must be called after each call of MODE, STRU or TYPE */ public void setCorrectCodec() { FtpDataModeCodec modeCodec = (FtpDataModeCodec) channelPipeline .get(FtpDataInitializer.CODEC_MODE); FtpDataTypeCodec typeCodec = (FtpDataTypeCodec) channelPipeline .get(FtpDataInitializer.CODEC_TYPE); FtpDataStructureCodec structureCodec = (FtpDataStructureCodec) channelPipeline .get(FtpDataInitializer.CODEC_STRUCTURE); if (modeCodec == null || typeCodec == null || structureCodec == null) { return; } modeCodec.setMode(session.getDataConn().getMode()); modeCodec.setStructure(session.getDataConn().getStructure()); typeCodec.setFullType(session.getDataConn().getType(), session .getDataConn().getSubType()); structureCodec.setStructure(session.getDataConn().getStructure()); logger.debug("codec setup"); } /** * Unlock the Mode Codec from openConnection of {@link FtpTransferControl} * */ public void unlockModeCodec() { FtpDataModeCodec modeCodec = (FtpDataModeCodec) channelPipeline .get(FtpDataInitializer.CODEC_MODE); modeCodec.setCodecReady(); } /** * Default exception task: close the current connection after calling exceptionLocalCaught. * */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (session == null) { logger.debug("Error without any session active {}", cause); return; } Throwable e1 = cause; if (e1 instanceof ConnectException) { ConnectException e2 = (ConnectException) e1; logger.warn("Connection impossible since {}", e2.getMessage()); } else if (e1 instanceof ChannelException) { ChannelException e2 = (ChannelException) e1; logger.warn("Connection (example: timeout) impossible since {}", e2 .getMessage()); } else if (e1 instanceof ClosedChannelException) { logger.debug("Connection closed before end"); } else if (e1 instanceof InvalidArgumentException) { InvalidArgumentException e2 = (InvalidArgumentException) e1; logger.warn("Bad configuration in Codec in {}", e2.getMessage()); } else if (e1 instanceof NullPointerException) { NullPointerException e2 = (NullPointerException) e1; logger.warn("Null pointer Exception", e2); try { if (dataBusinessHandler != null) { dataBusinessHandler.exceptionLocalCaught(e1); if (session.getDataConn() != null) { if (session.getDataConn().checkCorrectChannel(ctx.channel())) { session.getDataConn().getFtpTransferControl() .setTransferAbortedFromInternal(true); } } } } catch (NullPointerException e3) { } return; } else if (e1 instanceof CancelledKeyException) { CancelledKeyException e2 = (CancelledKeyException) e1; logger.warn("Connection aborted since {}", e2.getMessage()); // XXX TODO FIXME is it really what we should do ? // No action return; } else if (e1 instanceof IOException) { IOException e2 = (IOException) e1; logger.warn("Connection aborted since {}", e2.getMessage()); } else if (e1 instanceof NotYetConnectedException) { NotYetConnectedException e2 = (NotYetConnectedException) e1; logger.debug("Ignore this exception {}", e2.getMessage()); return; } else if (e1 instanceof BindException) { BindException e2 = (BindException) e1; logger.warn("Address already in use {}", e2.getMessage()); } else if (e1 instanceof ConnectException) { ConnectException e2 = (ConnectException) e1; logger.warn("Timeout occurs {}", e2.getMessage()); } else { logger.warn("Unexpected exception from Outband: {}", e1.getMessage(), e1); } if (dataBusinessHandler != null) { dataBusinessHandler.exceptionLocalCaught(e1); } if (session.getDataConn().checkCorrectChannel(ctx.channel())) { session.getDataConn().getFtpTransferControl() .setTransferAbortedFromInternal(true); } } public void setFtpTransfer(FtpTransfer ftpTransfer) { this.ftpTransfer = ftpTransfer; } /** * Act as needed according to the receive DataBlock message * */ @Override public void channelRead0(ChannelHandlerContext ctx, DataBlock dataBlock) { if (ftpTransfer == null) { try { ftpTransfer = session.getDataConn().getFtpTransferControl().getExecutingFtpTransfer(); } catch (FtpNoTransferException e) { logger.debug(e); session.getDataConn().getFtpTransferControl() .setTransferAbortedFromInternal(true); } if (ftpTransfer == null) { logger.debug("No ExecutionFtpTransfer found"); session.getDataConn().getFtpTransferControl() .setTransferAbortedFromInternal(true); return; } } try { if (isStillAlive()) { try { ftpTransfer.getFtpFile().writeDataBlock(dataBlock); } catch (FtpNoFileException e1) { logger.debug(e1); session.getDataConn().getFtpTransferControl() .setTransferAbortedFromInternal(true); return; } catch (FileTransferException e1) { logger.debug(e1); session.getDataConn().getFtpTransferControl() .setTransferAbortedFromInternal(true); } } else { // Shutdown session.getDataConn().getFtpTransferControl() .setTransferAbortedFromInternal(true); WaarpSslUtility.closingSslChannel(ctx.channel()); } } finally { dataBlock.getBlock().release(); } } /** * Write a simple message (like LIST) and wait for it * * @param message * @return True if the message is correctly written */ public boolean writeMessage(String message) { DataBlock dataBlock = new DataBlock(); dataBlock.setEOF(true); ByteBuf buffer = Unpooled.wrappedBuffer(message.getBytes(WaarpStringUtils.UTF8)); dataBlock.setBlock(buffer); ChannelFuture future; logger.debug("Will write: " + buffer.toString(WaarpStringUtils.UTF8)); try { future = dataChannel.writeAndFlush(dataBlock); future.await(FtpConfiguration.getDATATIMEOUTCON()); } catch (InterruptedException e) { logger.debug("Interrupted", e); return false; } logger.debug("Write result: " + future.isSuccess(), future.cause()); return future.isSuccess(); } /** * If the service is going to shutdown, it sends back a 421 message to the connection * * @return True if the service is alive, else False if the system is going down */ private boolean isStillAlive() { if (session.getConfiguration().isShutdown()) { session.setExitErrorCode("Service is going down: disconnect"); return false; } return true; } }